export class JsCodeEncoder {
  children: JsCodeEncoder[]
  tabCount: number
  encode() {
    return this.children.map((child) => child.encode()).join("\n")
  }
  constructor(children: JsCodeEncoder[]) {
    this.children = children.filter((item) => !!item)
    this.tabCount = 0
  }

  get tab() {
    return new Array(this.tabCount).fill('\t').join('')
  }
}

export class ObjectEncoder extends JsCodeEncoder {
  propertiesEncoder: ObjectPropertiesEncoder
  methodEncoders: ObjectMethodEncoder[]
  constructor(options: {
    propertiesEncoder?: ObjectPropertiesEncoder
    methodEncoders?: ObjectMethodEncoder[]
  } = {}) {
    var propertiesEncoder = options.propertiesEncoder || new ObjectPropertiesEncoder([])
    var methodEncoders =  options.methodEncoders || []
    super([propertiesEncoder, ...methodEncoders])
    this.propertiesEncoder = propertiesEncoder
    this.methodEncoders = methodEncoders
  }

  encode() {
    this.propertiesEncoder.tabCount = this.tabCount + 1
    this.methodEncoders.forEach((encoder) => {
      encoder.tabCount = this.tabCount + 1
    })
    var tab = this.tab
    var methods = this.methodEncoders.map((item) => item.encode()).join(`,\n`)
    var properties = this.propertiesEncoder.encode()
    return `{\n${[properties, methods].filter(item => !!item).join(`,\n${tab}`)}\n${tab}}`
  }

  get methodEncoderMap() {
    var map: {[key:string]: ObjectMethodEncoder} = {}
    this.methodEncoders.forEach((encoder: ObjectMethodEncoder) => {
      map[encoder.name] = encoder
    })
    return map
  }

  merge(objectEncoder: ObjectEncoder) {
    if(objectEncoder == null) return
    this.propertiesEncoder.merge(objectEncoder.propertiesEncoder)

    var allKeys = Array.from(new Set(Object.keys(this.methodEncoderMap).concat(Object.keys(objectEncoder.methodEncoderMap)))) 
    var methodEncoders = []
    allKeys.forEach((key: string) => {
      var encoder = new ObjectMethodEncoder(key, {})
      encoder.merge(this.methodEncoderMap[encoder.name])
      encoder.merge(objectEncoder.methodEncoderMap[encoder.name])
      methodEncoders.push(encoder)
    })
    this.methodEncoders = methodEncoders
  }


  addProperty(name: string, value: string | ObjectEncoder) {
    this.propertiesEncoder.addProperty(name, value)
    return this
  }
  
  /** 添加属性名称和值都同名的属性 */
  addSameNameProperty(name: string) {
    var value = name
    this.addProperty(name, value)
    return this
  }

  addMethod(name: string, defines: string[], returnObj: ObjectEncoder, argumentEncoder?: MethodArgumentEncoder) {
    this.methodEncoders.push(new ObjectMethodEncoder(name, {
      defineEncoder: new MethodDefineEncoder(defines),
      returnEncoder: returnObj == null ? null : new MethodReturnObjectEncoder({ returnObject: returnObj }),
      argumentEncoder: argumentEncoder
    }))
    return this
  }
}

export class ObjectPropertiesEncoder extends JsCodeEncoder {
  properties: ObjectPropertyEncoder[]
  constructor(properties: ObjectPropertyType[]) {
    super([])
    this.properties = properties.map(item =>  new ObjectPropertyEncoder(item.name, item.value))
  }
  encode(): string {
    var tab = this.tab
    this.properties.forEach((item) => {
      item.tabCount = this.tabCount + 1
    })
    return `${this.properties.map((item) => `${tab}${item.encode()}`).join(',\n')}`
  }

  merge(propertiesEncoder: ObjectPropertiesEncoder) {
    if(propertiesEncoder == null) return
    var keys = Array.from(new Set(Object.keys(this.propertiesMap).concat(Object.keys(propertiesEncoder.propertiesMap))))
    var propertiesMap : {[key: string]: ObjectPropertyEncoder} = {}
    keys.forEach((key) => {
      if(propertiesEncoder.propertiesMap[key] instanceof ObjectPropertyEncoder && this.propertiesMap[key] instanceof ObjectPropertyEncoder) {
        this.propertiesMap[key].merge(propertiesEncoder.propertiesMap[key])
        propertiesMap[key] = this.propertiesMap[key]
      } else if(propertiesEncoder.propertiesMap[key] instanceof ObjectPropertyEncoder) {
        propertiesMap[key] = propertiesEncoder.propertiesMap[key]
      } else if(this.propertiesMap[key] instanceof ObjectPropertyEncoder) {
        propertiesMap[key] = this.propertiesMap[key]
      } else {
        propertiesMap[key] = propertiesEncoder.propertiesMap[key] || this.propertiesMap[key]
      }
    })
    this.properties =  keys.map((key) => propertiesMap[key])
  }
  addProperty(name: string, value: string | ObjectEncoder) {
    if(this.properties.find((item) => item.name === name)) {
      this.updateProperty(name, value)
      return
    }
    this.properties.push(new ObjectPropertyEncoder(name, value))
  }

  updateProperty(name: string, value: string | ObjectEncoder) {
    var prop = this.properties.find((item) => item.name === name)
    prop.value = value
  }

  get propertiesMap() {
    var map : {[key: string]: ObjectPropertyEncoder} = {}
    this.properties.forEach((prop) => {
      map[prop.name] = prop
    })
    return map
  }
}

export type ObjectPropertyType = {
  name: string
  value: string | JsCodeEncoder
}

export class ObjectPropertyEncoder extends JsCodeEncoder {
  name: string
  value: string | JsCodeEncoder
  constructor(name: string, value: string | JsCodeEncoder) {
    super([])
    this.name = name
    this.value = value
  }

  encode() {
    if(this.value instanceof JsCodeEncoder) {
      this.value.tabCount = 1
      return `${this.name}: ${this.value.encode()}`
    } else {
      if(this.name === this.value) {
        return `${this.name}`
      }
      return `${this.name}: ${this.value}`
    }
  }

  merge(propertyEncoder: ObjectPropertyEncoder) {
    if(this.value instanceof ObjectEncoder && propertyEncoder.value instanceof ObjectEncoder) {
      this.value.merge(propertyEncoder.value)
    } else if(propertyEncoder.value instanceof ObjectEncoder) {
      this.value = propertyEncoder.value
    } else if(this.value instanceof ObjectEncoder) {
      // do nothing
    } else {
      this.value = this.value || propertyEncoder.value
    }
  }
}

export class ObjectMethodEncoder extends JsCodeEncoder {
  name: string
  argumentEncoder?: MethodArgumentEncoder
  defineEncoder?: MethodDefineEncoder
  returnEncoder?: MethodReturnEncoder
  constructor(name: string, options: {
    argumentEncoder?: MethodArgumentEncoder, 
    defineEncoder?: MethodDefineEncoder, 
    returnEncoder?: MethodReturnEncoder
  }) {
    super([options.argumentEncoder, options.defineEncoder, options.returnEncoder])
    this.argumentEncoder = options.argumentEncoder || new MethodArgumentEncoder([])
    this.defineEncoder = options.defineEncoder || new MethodDefineEncoder([])
    this.returnEncoder = options.returnEncoder || new MethodReturnEncoder("")
    this.name = name
  }
  encode(): string  {
    if(this.defineEncoder) this.defineEncoder.tabCount = this.tabCount + 1
    if(this.returnEncoder) this.returnEncoder.tabCount = this.tabCount + 1
    var tab = this.tab
    var defines = this.defineEncoder?.encode()
    var returns = this.returnEncoder?.encode()
    return `${tab}${this.name} (${this.argumentEncoder?.encode()}) {${defines ? `\n${defines}` : ''}${returns ? `\n${returns}` : ''}\n${tab}}`
  }

  merge(objectMethodEncoder: ObjectMethodEncoder) {
    if(objectMethodEncoder == null) return
    this.argumentEncoder?.merge(objectMethodEncoder.argumentEncoder)
    this.defineEncoder?.merge(objectMethodEncoder.defineEncoder)
    if(objectMethodEncoder.returnEncoder instanceof MethodReturnObjectEncoder) {
      objectMethodEncoder.returnEncoder.merge(this.returnEncoder)
      this.returnEncoder = objectMethodEncoder.returnEncoder
    } else {
      this.returnEncoder.merge(objectMethodEncoder.returnEncoder)
    }
  }
}

export type MethodArgument = {
  name: string
  type?: string
}

export class MethodArgumentEncoder extends JsCodeEncoder {
  args: MethodArgument[]
  constructor(args: MethodArgument[]) {
    super([])
    this.args = args
  }
  encode(): string {
      return `${this.args.map((item) => `${item.name}${item.type ? `: ${item.type}` : ''}`).join(',')}`
  }

  merge(argumentEncoder: MethodArgumentEncoder) {
    if(argumentEncoder == null) return
    this.addArgs(argumentEncoder.args)
  }

  addArg(arg: MethodArgument) {
    if(this.args.find((item) =>item.name === arg.name)) return
    this.args.push(arg)
  }

  addArgs(args: MethodArgument[]) {
    args.forEach((arg) => this.addArg(arg))
  }
}

export class MethodDefineEncoder extends JsCodeEncoder {
  defines: string[]
  constructor(defines: string[]) {
    super([])
    this.defines = defines || []
  }
  encode() {
    var tab = this.tab
    var defines = this.defines.join('\n')
    return defines.split('\n').map((item) => `${tab}${item}`).join("\n")
  }

  merge(defines: MethodDefineEncoder) {
    this.addImports(defines.defines)
  }
  addImport(define: string) {
    if(this.defines.includes(define)) return
    this.defines.push(define)
  }
  addImports(defines: string[]) {
    defines.forEach((define: string) => this.addImport(define))
  }
}

export class MethodReturnEncoder extends JsCodeEncoder {
  returnValue: string
  constructor(returnValue: string) {
    super([])
    this.returnValue = returnValue
  }
  encode() {
    if(!this.returnValue) {return ''}
    return `return ${this.returnValue}`
  }

  merge(returnEncoder: MethodReturnEncoder) {
    this.returnValue =  returnEncoder.returnValue
  }
}

export class MethodReturnObjectEncoder extends MethodReturnEncoder {
  returnObject: ObjectEncoder
  constructor(options: { returnObject?: ObjectEncoder }) {
    super("")
    this.returnObject = options.returnObject || new ObjectEncoder()
    this.returnValue = this.returnObject.encode()
  }
  encode() {
    this.returnObject.tabCount = 0
    this.returnValue = this.returnObject.encode()
    var tab = this.tab
    var content = this.returnValue.split('\n').join(`\n${tab}`)
    return `${tab}return ${content}`
  }

  merge(returnEncoder: MethodReturnObjectEncoder | MethodReturnEncoder): void {
      if(returnEncoder == null) return
      if(returnEncoder instanceof MethodReturnObjectEncoder) {
        this.returnObject.merge(returnEncoder.returnObject)
      }
  }
}

export class VueJsCodeEncoder extends JsCodeEncoder {
  defines: MethodDefineEncoder
  exportDefault: ObjectEncoder

  constructor(defines?: MethodDefineEncoder, exportDefault?: ObjectEncoder) {
    super([
      defines,
      exportDefault
    ])
    this.defines = defines || new MethodDefineEncoder([])
    this.exportDefault = exportDefault || new ObjectEncoder({})
    this.defines.tabCount = 0
    this.exportDefault.tabCount = 0
  }

  encode() {
    return `${this.defines.encode()}\nexport default ${this.exportDefault.encode()}`
  }

  merge(encoder: VueJsCodeEncoder) {
    this.defines.merge(encoder.defines)
    this.exportDefault.merge(encoder.exportDefault)
  }

  addImport(define: string) {
    this.defines.addImport(define)
    return this
  }

  addImports(defines: string[]) {
    this.defines.addImports(defines)
    return this
  }

  addProperty(name: string, value: string | ObjectEncoder) {
    this.exportDefault.addProperty(name, value)
    return this
  }

  addMethod(name: string, defines: string[], returnObj: ObjectEncoder) {
    this.exportDefault.addMethod(name, defines, returnObj)
    return this
  }
}
